<template>
  <div class="mobile-upload-main">
    <div class="main-inner">
      <div class="inner-buttons">
        <van-button type="default" icon="add-o" size="small" :disabled="changeDisabled">
          选择文件
          <input v-if="!changeDisabled" type="file" capture="camera" :multiple="multiple" class="select-file-input" :accept="accept" οnclick="f.outerHTML=f.outerHTML" @change="handleFileChange" />
        </van-button>
        <van-button type="default" size="small" icon="passed" :disabled="uploadDisabled" @click="handleUpload">上传</van-button>
        <!-- <van-button type="default" icon="pause-circle-o" size="small" :disabled="pauseDisabled" @click="handlePause">暂停</van-button>
        <van-button type="default" icon="play-circle-o" size="small" :disabled="resumeDisabled" @click="handleResume">恢复</van-button>-->
        <van-button type="default" icon="delete-o" size="small" :disabled="clearDisabled" @click="clearFiles">清空</van-button>
      </div>
      <div class="inner-list">
        <van-collapse v-if="uploadFiles.length" v-model="activeName" accordion>
          <van-collapse-item v-for="(item, index) in uploadFiles" :key="index">
            <template #title>
              <div class="list-item">
                <div class="item-name">{{ item.name }}</div>
                <div class="item-status">
                  <i :class="fileStatuIcon(item.status)" />
                  {{ item.status | fileStatus }}
                </div>
              </div>
            </template>
            <div class="list-box">
              <div class="box-size">大小：{{ item.size | transformByte }}</div>
              <div v-if="item.hashProgress !== 100" class="box-progress">
                <span>{{ status === 'read' ? '正在读取文件' : '正在生成切片' }}：</span>
                <van-progress :percentage="item.hashProgress" />
              </div>
              <div v-else class="box-progress">
                <span>上传文件进度：</span>
                <van-progress :percentage="item.uploadProgress" />
              </div>
            </div>
          </van-collapse-item>
        </van-collapse>
        <slot name="tip" />
      </div>
    </div>
  </div>
</template>
<script>
import axios, { CancelToken } from 'axios'
import Qs from 'qs'

var instance = axios.create({
  transformRequest: [(data) => {
    data = Qs.stringify(data)
    return data
  }]
})

// 所有文件状态
const Status = {
  read: 'read',
  wait: 'wait',
  pause: 'pause',
  uploading: 'uploading',
  hash: 'hash',
  error: 'error',
  done: 'done'
}

// 单个文件的状态
const fileStatus = {
  read: 'read',
  wait: 'wait',
  uploading: 'uploading',
  success: 'success',
  error: 'error',
  secondPass: 'secondPass',
  pause: 'pause',
  resume: 'resume'
}

// 单个文件的状态 对应描述
const fileStatusStr = {
  read: '文件读取中',
  wait: '待上传',
  uploading: '上传中',
  success: '成功',
  error: '失败',
  secondPass: '已秒传',
  pause: '暂停',
  resume: '恢复'
}

export default {
  name: 'MobileUploader',
  filters: {
    transformByte(size) {
      if (!size) {
        return '0B'
      }

      var num = 1024.0 // byte

      if (size < num) {
        return size + 'B'
      }
      if (size < Math.pow(num, 2)) {
        return (size / num).toFixed(2) + 'K'
      } // kb
      if (size < Math.pow(num, 3)) {
        return (size / Math.pow(num, 2)).toFixed(2) + 'M'
      } // M
      if (size < Math.pow(num, 4)) {
        return (size / Math.pow(num, 3)).toFixed(2) + 'G'
      } // G
      return (size / Math.pow(num, 4)).toFixed(2) + 'T' // T
    },
    // 单个文件状态格式化
    fileStatus(status) {
      return fileStatusStr[fileStatus[status]]
    }
  },
  props: {
    headers: {
      type: Object,
      default: null
    },
    beforeUpload: {
      type: Function,
      default: null
    },
    // 支持文件类型
    accept: {
      type: String,
      default: ''
    },
    // 上传文件时携带的参数
    uploadArguments: {
      type: Object,
      default: () => {
        return {}
      }
    },
    // 是否传递cookie
    withCredentials: {
      type: Boolean,
      default: false
    },
    // 文件个数
    limit: {
      type: Number,
      default: 1
    },
    // 文件超出个数限制时的钩子
    onExceed: {
      type: Function,
      default: () => { }
    },
    // 多文件上传
    multiple: {
      type: Boolean,
      default: true
    },
    // axios baseUrl
    baseUrl: {
      type: String,
      default: ''
    },
    // 切片大小
    chunkSize: {
      type: Number,
      default: 10 * 1024 * 1024
    },
    // 上传并发数
    threads: {
      type: Number,
      default: 2
    },
    // 错误重试次数
    chunkRetry: {
      type: Number,
      default: 3
    },
    // 是否显示切片上传进度
    showChunks: {
      type: Boolean,
      default: false
    }
  },
  data: () => ({
    uploadFiles: [],
    fileIndex: 0, // 当前正在被遍历的文件下标
    worker: null,
    cancels: [], // 存储要取消的请求
    _threads: 2,
    status: Status.read, // 默认状态
    hide: false,
    _axiosworker: null,
    activeName: ''
  }),
  computed: {
    changeDisabled() {
      return ![Status.read, Status.wait, Status.done].includes(this.status)
    },
    uploadDisabled() {
      return [Status.read, Status.pause, Status.done, Status.uploading, Status.hash].includes(this.status)
    },
    pauseDisabled() {
      return [Status.read, Status.hash, Status.select, Status.wait, Status.done, Status.pause].includes(this.status)
    },
    resumeDisabled() {
      return ![Status.pause].includes(this.status)
    },
    clearDisabled() {
      return !this.uploadFiles.length
    },
    fileStatuIcon(status) {
      return (status) => {
        let name = ''
        switch (status) {
          case 'read':
          case 'uploading':
            name = 'fa fa-spinner fa-spin'
            break
          case 'success':
          case 'secondPass':
            name = 'fa fa-check-circle-o'
            break
          case 'error':
            name = 'fa fa-ban'
            break
        }
        return name
      }
    }
  },
  created() {
    this.setAxios()
    this._threads = this.threads
  },
  destroyed() {
    this.fileIndex = 0
    this.clearFiles()
  },
  methods: {
    handleFileChange(e) {
      const files = e.target.files
      if (!files) return
      // 判断文件选择的个数
      if (this.limit && files.length > this.limit) {
        this.$toast.fail(
          `最多支持选择${this.limit}个文件`
        )
        return
      }
      if ((this.uploadFiles.length + 1) > this.limit) {
        this.$toast.fail(
          `最多支持上传${this.limit}个文件`
        )
        return
      }
      this.status = Status.read
      Array.prototype.slice.call(files).forEach((item, i) => {
        this.handleStart(item)
      })
    },

    async handleStart(file) {
      // 初始化部分自定义属性
      file.status = fileStatus.read
      file.chunks = []
      file.uploadProgress = 0
      file.fakeUploadProgress = 0 // 假进度条，处理恢复上传后，进度条后移的问题
      file.hashProgress = 0
      if (this.beforeUpload) {
        const before = this.beforeUpload(file)
        if (before && before.then) {
          before.then((res) => {
            this.uploadFiles.push(file)
          })
        }
      }
      if (!this.beforeUpload) {
        this.uploadFiles.push(file)
      }
      const chunknum = Math.ceil(file.size / this.chunkSize)
      const name = file.name
      const size = file.size
      const md5 = await this.calculateHash(this.createFileChunk(file))
      const obj = {
        md5: md5,
        fileName: name,
        fileSize: size,
        chunkNum: chunknum,
        chunkSize: this.chunkSize
      }
      await this.getUUID(obj).then((data) => {
        this.uploadFiles = this.uploadFiles.map((a, index) => {
          if (index === this.fileIndex) {
            a['UUID'] = data
            a['status'] = fileStatus.wait
          }
          return a
        })
      })
      this.fileIndex += 1
      this.status = Status.wait
    },

    async handleUpload() {
      if (!this.uploadFiles) return
      this.status = Status.uploading
      const files = this.uploadFiles
      for (let i = 0; i < files.length; i++) {
        this.fileIndex = i
        if (['secondPass', 'success', 'error'].includes(files[i].status)) {
          console.log('跳过已上传成功或已秒传的或失败的')
          continue
        }

        const chunks = this.createFileChunk(files[i])

        this.status = Status.uploading

        files[i].status = fileStatus.uploading

        let already = []
        // 获取已经上传成功的切片
        await this.getAlreadyChunk({ UUID: files[i].UUID }).then((data) => {
          already = Object.assign([], data)
        })

        files[i].chunks = chunks.map(({ file }, index) => ({
          UUID: files[i].UUID,
          fileName: files[i].name,
          index,
          chunk: file,
          size: file.size,
          uploaded: already && already.includes(index + ''), // 标识：是否已完成上传
          progress: already && already.includes(index + '') ? 100 : 0,
          status: already && already.includes(index + '') ? 'success' : 'wait' // 上传状态，用作进度状态显示
        }))

        this.$set(files, i, files[i])

        await this.uploadChunks(files[i])
      }
    },

    /**
     * 将切片传输给服务端
     */
    async uploadChunks(data) {
      var chunkData = data.chunks
      return new Promise(async (resolve, reject) => {
        const requestDataList = chunkData
          .filter(({ uploaded }) => !uploaded)
          .map(({ chunk, UUID, fileName, index }) => {
            const formData = new FormData()
            formData.append('UUID', UUID)
            formData.append('file', chunk)
            formData.append('fileIndex', index) // 文件名使用切片的下标
            return { formData, index, fileName, UUID }
          })

        try {
          const ret = await this.sendRequest(requestDataList, chunkData)
          console.log('ret', ret)
        } catch (error) {
          this.$toast.fail('亲 上传失败了,考虑重试下呦' + error)
          return
        }

        // 合并切片
        const isUpload = chunkData.some((item) => item.uploaded === false)
        if (isUpload) {
          alert('存在失败的切片')
        } else {
          // 执行合并
          try {
            await this.mergeRequest(data)
            resolve()
          } catch (error) {
            reject()
          }
        }
      })
    },

    /**
     * 并发处理
     */
    sendRequest(forms, chunkData) {
      var finished = 0
      const total = forms.length
      const that = this
      const retryArr = [] // 数组存储每个文件hash请求的重试次数，做累加 比如[1,0,2],就是第0个文件切片报错1次，第2个报错2次

      return new Promise((resolve, reject) => {
        const handler = () => {
          if (forms.length) {
            // 出栈
            const formInfo = forms.shift()
            const formData = formInfo.formData
            const index = formInfo.index
            instance.post('/API-DOCUMENT/document/file/chunk', formData, {
              onUploadProgress: that.createProgresshandler(chunkData[index]),
              cancelToken: new CancelToken((c) => this.cancels.push(c)),
              timeout: 10000000,
              transformRequest: [(data) => {
                return data
              }],
              headers: {
                'Content-Type': 'multipart/form-data'
              }
            }).then((res) => {
              console.log('handler -> res', res)
              // 更改状态
              chunkData[index].uploaded = true
              chunkData[index].status = 'success'

              finished++
              handler()
            }).catch((e) => {
              // 若状态为暂停或等待，则禁止重试
              if ([Status.pause, Status.wait].includes(this.status)) return

              if (typeof retryArr[index] !== 'number') {
                retryArr[index] = 0
              }

              // 更新状态
              chunkData[index].status = 'warning'

              // 累加错误次数
              retryArr[index]++

              // 重试3次
              if (retryArr[index] >= this.chunkRetry) {
                console.warn(' 重试失败--- > handler -> retryArr', retryArr, chunkData[index].hash)
                return reject('重试失败', retryArr)
              }

              console.log('handler -> retryArr[finished]', `${chunkData[index].hash}--进行第 ${retryArr[index]} '次重试'`)
              console.log(retryArr)

              this._threads++ // 释放当前占用的通道

              // 将失败的重新加入队列
              forms.push(formInfo)
              handler()
            })
          }

          if (finished >= total) {
            resolve('done')
          }
        }

        // 控制并发
        for (let i = 0; i < this._threads; i++) {
          handler()
        }
      })
    },

    /**
     * 提交文件获取UUID
     */
    async getUUID(params) {
      return new Promise((resolve, reject) => {
        instance.post('/API-DOCUMENT/document/file/insert', params, {
          timeout: 0
        }).then((res) => {
          if (res.data.code === 200 || res.data.code === '200') {
            resolve(res.data.data)
          } else {
            this.$toast.fail(res.data.msg || '文件读取失败')
            resolve()
          }
        }).catch((err) => {
          reject(err)
        })
      })
    },

    /**
     * 获取已经上传成功的切片
     */
    async getAlreadyChunk(params) {
      return new Promise((resolve, reject) => {
        instance.post('/API-DOCUMENT/document/file/getIndexArray', params, {
          timeout: 0
        }).then((res) => {
          if (res.data.code === 200 || res.data.code === '200') {
            resolve(res.data.data)
          } else {
            this.$toast.fail(res.data.msg || '获取已上传切片失败')
            resolve()
          }
        }).catch((err) => {
          reject(err)
        })
      })
    },

    /**
     * 通知服务端合并切片
     */
    mergeRequest(data) {
      return new Promise((resolve, reject) => {
        instance.post('/API-DOCUMENT/document/file/merge', { UUID: data.UUID }, {
          timeout: 10000000
        }).then((res) => {
          if (res.data.code === 200 || res.data.code === '200') {
            data.status = fileStatus.success
            data['id'] = res.data.data.id
            // 判断是否所有都成功上传
            this.isAllStatus()
            resolve()
          } else {
            data.status = fileStatus.error
            this.status = Status.wait
            resolve()
          }
        }).catch(() => {
          data.status = fileStatus.error
          reject()
        })
      })
    },

    /**
     * 创建文件切片
     */
    createFileChunk(file, size = this.chunkSize) {
      const list = []
      var count = 0
      while (count < file.size) {
        list.push({
          file: file.slice(count, count + size)
        })
        count += size
      }
      return list
    },

    /**
     * 暂停上传
     */
    handlePause() {
      this.status = Status.pause
      if (this.uploadFiles.length) {
        const currentFile = this.uploadFiles[this.fileIndex]
        currentFile.status = fileStatus.pause
        // 将当前进度赋值给假进度条
        currentFile.fakeUploadProgress = currentFile.uploadProgress
      }
      while (this.cancels.length > 0) {
        this.cancels.pop()('取消请求')
      }
    },

    /**
     * 恢复上传
     */
    handleResume() {
      this.status = Status.uploading
      this.uploadFiles[this.fileIndex].status = fileStatus.resume
      this.handleUpload()
    },

    /**
     * 生成文件 hash（web-worker）
     */
    calculateHash(chunks) {
      return new Promise((resolve) => {
        this.worker = new Worker('./hash.js')
        this.worker.postMessage({ chunks })
        this.worker.onmessage = (e) => {
          const { percentage, hash } = e.data
          if (this.uploadFiles[this.fileIndex]) {
            this.uploadFiles[this.fileIndex].hashProgress = Number(percentage.toFixed(0))
            this.$set(this.uploadFiles, this.fileIndex, this.uploadFiles[this.fileIndex])
          }
          if (hash) {
            resolve(hash)
          }
        }
      })
    },

    /**
     * 切片上传进度
     */
    createProgresshandler(item) {
      return (p) => {
        item.progress = parseInt(String((p.loaded / p.total) * 100))
        this.fileProgress()
      }
    },

    /**
     * 文件总进度
     */
    fileProgress() {
      const currentFile = this.uploadFiles[this.fileIndex]
      if (currentFile) {
        const uploadProgress = currentFile.chunks.map((item) => item.size * item.progress).reduce((acc, cur) => acc + cur)
        const currentFileProgress = parseInt((uploadProgress / currentFile.size).toFixed(2))

        // 真假进度条处理--处理进度条后移
        if (!currentFile.fakeUploadProgress) {
          currentFile.uploadProgress = currentFileProgress
          this.$set(this.uploadFiles, this.fileIndex, currentFile)
        } else if (currentFileProgress > currentFile.fakeUploadProgress) {
          currentFile.uploadProgress = currentFileProgress
          this.$set(this.uploadFiles, this.fileIndex, currentFile)
        }
      }
    },

    /**
     * axios 设置axios参数
     */
    setAxios() {
      if (!this.headers) return
      for (const i in this.headers) {
        instance.defaults.headers.common[i] = this.headers[i]
      }

      // 是否携带cookie
      if (this.withCredentials) {
        instance.defaults.withCredentials = true
      }

      // 设置baseUrl
      if (this.baseUrl) {
        instance.defaults.baseURL = this.baseUrl
      }
    },

    /**
     * 清空文件
     */
    clearFiles() {
      this.fileIndex = 0
      this.handlePause()

      this.worker && this.worker.terminate() // 中断worker
      this._axiosworker && this._axiosworker.terminate() // 中断_axiosworker

      this.status = Status.wait

      Object.assign(this.$data, this.$options.data()) // 重置data所有数据
    },

    /**
     * 判断是否都已完成上传
     */
    isAllStatus() {
      const isAllSuccess = this.uploadFiles.every((item) => ['success', 'secondPass', 'error'].includes(item.status))
      if (isAllSuccess) {
        this.status = Status.done
        const arr = []
        this.uploadFiles.forEach(a => {
          arr.push(a.id)
        })
        this.$emit('success', arr.join(','))
      }
    }
  }
}
</script>
<style lang="scss">
$font-size: 75;
@function rem($px) {
  @return $px / $font-size * 1rem;
}
.mobile-upload-main {
  position: relative;
  width: 100%;
  height: 100%;
  overflow: hidden;

  .main-inner {
    width: 90%;
    margin: rem(20) auto;
    border: 1px solid #d2d2d2;
    border-radius: rem(4);
    background-color: #fff;
    padding: rem(20);
    overflow: hidden;

    .inner-buttons {
      margin-bottom: rem(20);
      position: relative;
      display: flex;
      align-items: center;
      .select-file-input {
        position: absolute;
        display: inline-block;
        left: 0;
        top: 0;
        border: none;
        opacity: 0;
        width: 100%;
        height: 100%;
      }
    }

    .inner-list {
      .list-item {
        padding: rem(10) 0;
        position: relative;
        font-size: rem(24);
        display: flex;
        align-items: center;
        justify-content: space-between;
        .item-name {
          font-size: rem(24);
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
          margin-right: rem(5);
          .svg-icon {
            font-size: rem(24);
            vertical-align: sub;
          }
        }

        .item-status {
          font-size: rem(24);
          text-align: center;
          text-align: left;
          flex-shrink: 0;
          .el-icon-circle-check {
            color: #67c23a;
          }
          .el-icon-circle-close {
            color: #f00;
          }
        }
      }
      .list-box {
        z-index: 10;
        font-size: rem(24);
        .box-size {
          overflow: hidden;
          text-overflow: ellipsis;
          white-space: nowrap;
        }
        .box-progress {
          display: flex;
          align-items: center;
          > span {
            flex-shrink: 0;
          }
          .van-progress {
            flex: 1;
          }
        }
      }
      .van-cell__title {
        flex: 0 0 92%;
        overflow: hidden;
      }
      .van-collapse-item__content {
        padding: rem(10);
        line-height: rem(50);
      }
      .van-cell {
        padding: 0 !important;
        display: flex;
        align-items: center;
      }
    }
  }

  .upload-tip {
    font-size: rem(24);
    color: #606266;
    margin-top: rem(10);
    display: flex;
    align-items: center;
  }

  .el-progress {
    width: 80%;
    display: inline-block;
  }
  >>> .el-collapse-item__header {
    height: auto;
  }
}
</style>
